/*
 * Copyright 2022 The Open Islands Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openislands.oi.config.stateengine;

import org.openislands.oi.constant.Dict;
import org.openislands.oi.constant.StateRuleEnum;
import org.openislands.oi.util.EnumUtils;
import org.openislands.oi.util.YamlUtils;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;

@Configuration
public class StateEngineContext {
    /***
     * cache all keys and corresponding collection under Dict.STATE_COLLECTION
     */
    private final Map<String, Set<String>> stateSetCache = new HashMap<>();

    /***
     * cache rule types corresponding to all rule lists Convertible lists
     */
    private final Map<StateRuleEnum, List<StateCollection>> stateCollectionCache = new HashMap<>();

    /***
     * cache all states and transition able state lists under all rule types
     */
    private final Map<StateRuleEnum, Map<String, StateCollection>> stateLoadCache = new HashMap<>();

    @PostConstruct
    @SuppressWarnings("unchecked")
    public void load() throws IOException {
        Properties properties = YamlUtils.loadYamlProperties(Dict.TASK_STATE_RULES);
        Object stateCollection = properties.get(Dict.STATE_COLLECTION);

        // create state collection
        if (stateCollection instanceof Map) {
            for (Entry<String, Object> objectEntry : ((Map<String, Object>) stateCollection).entrySet()) {
                String key = objectEntry.getKey().toUpperCase();
                Set<String> stateSet = this.stateSetCache.get(key);
                if (Objects.isNull(stateSet)) {
                    stateSet = new HashSet<>();
                }

                Object value = objectEntry.getValue();
                if (value instanceof String) {
                    stateSet.add(value.toString().toUpperCase());
                } else if (value instanceof List) {
                    stateSet.addAll(((List<String>) value).stream().map(String::toUpperCase).collect(Collectors.toList()));
                }
                this.stateSetCache.put(key, stateSet);
            }

            boolean retry;
            do {
                retry = false;
                // analyze add sub collection
                for (Entry<String, Set<String>> entry : this.stateSetCache.entrySet()) {
                    Set<String> entryValue = entry.getValue();
                    Iterator<String> iterator = entryValue.iterator();
                    Set<Set<String>> addSet = new HashSet<>();
                    while (iterator.hasNext()) {
                        String state = iterator.next();
                        if (this.stateSetCache.containsKey(state)) {
                            if (!(entry.getKey()).equals(state)) {
                                retry = true;
                                addSet.add(this.stateSetCache.get(state));
                            }

                            iterator.remove();
                        }
                    }

                    addSet.forEach(entryValue::addAll);
                }
            } while (retry);
        }

        // parse state rule, collection to collection
        for (StateRuleEnum stateRuleEnum : StateRuleEnum.values()) {
            Object transitionRules = properties.get(stateRuleEnum.getConfig());

            List<StateCollection> collectionList = new ArrayList<>();
            if (transitionRules instanceof Map) {
                for (Entry<String, Object> entry : ((Map<String, Object>) transitionRules).entrySet()) {
                    // changeable target set
                    Object entryValue = entry.getValue();
                    Set<Set<String>> transitionStateSet = new HashSet<>();
                    if (entryValue instanceof String) {
                        transitionStateSet.add(Collections.singleton(entryValue.toString().toUpperCase()));
                    } else if (entryValue instanceof List) {
                        for (String state : ((List<String>) entryValue)) {
                            Set<String> stateSet = this.stateSetCache.get(state.toUpperCase());
                            if (Objects.isNull(stateSet)) {
                                transitionStateSet.add(Collections.singleton(state.toUpperCase()));
                            } else {
                                transitionStateSet.add(stateSet);
                            }
                        }
                    }

                    // set of states that need to be changed
                    final StateCollection[] convertibleArray = transitionStateSet.stream()
                            .map(StateCollection::new).toArray(StateCollection[]::new);
                    StateCollection newStateCollection = new StateCollection(convertibleArray);
                    String entryKey = entry.getKey().toUpperCase();
                    Set<String> stateSet = this.stateSetCache.get(entryKey);
                    if (Objects.isNull(stateSet)) {
                        newStateCollection.add(entryKey);
                    } else {
                        newStateCollection.addAll(stateSet);
                    }
                    collectionList.add(newStateCollection);
                }
            }

            this.stateCollectionCache.put(stateRuleEnum, collectionList);
        }

        // add matching rules for all states
        for (Entry<StateRuleEnum, List<StateCollection>> entry : this.stateCollectionCache.entrySet()) {
            Map<String, Set<StateCollection>> tempCollectionMap = new HashMap<>();
            Map<String, StateCollection> stateMap = new HashMap<>();
            final List<StateCollection> cacheList = entry.getValue();
            // add a list of transitions for all states
            for (StateCollection collection : cacheList) {
                for (String key : collection) {
                    Set<StateCollection> collectionSet = tempCollectionMap.get(key);
                    if (collectionSet == null) {
                        collectionSet = new HashSet<>();
                    }
                    if (Objects.nonNull(collection.getTransitionArray())) {
                        collectionSet.addAll(Arrays.asList(collection.getTransitionArray()));
                    }
                    tempCollectionMap.put(key, collectionSet);
                }
            }

            // create state collection
            for (Entry<String, Set<StateCollection>> setEntry : tempCollectionMap.entrySet()) {
                final StateCollection value = new StateCollection(setEntry.getValue().toArray(new StateCollection[0]));
                value.add(setEntry.getKey());
                stateMap.put(setEntry.getKey(), value);
            }

            this.stateLoadCache.put(entry.getKey(), stateMap);
        }
    }

    /***
     * get the state_collection corresponding to the key under the rule configuration state set
     * @param stateKey the key under the state_collection
     * @return state string set or null
     */
    public Set<String> getStateSet(String stateKey) {
        return this.stateSetCache.get(stateKey);
    }

    /***
     * press the state string set obtained by {@link #getStateSet(String)}
     * to obtain the enumerated state set contained by string fieldFunction
     * @param stateKey      {@link #getStateSet(String)}
     * @param clazz         transition enum class
     * @param fieldFunction an <p E> corresponding string field
     * @param <E>           an enum
     * @return Set<Enum>
     */
    public <E extends Enum<E>> Set<E> getStateSet(String stateKey, Class<E> clazz, Function<E, String> fieldFunction) {
        final Set<String> stateSet = this.getStateSet(stateKey);
        if (stateSet == null) return new HashSet<>();
        return EnumUtils.filter(clazz, e -> stateSet.contains(fieldFunction.apply(e)));
    }

    public List<Set<String>> getTransitionList(StateRuleEnum stateRuleEnum, String currentState) {
        Map<String, StateCollection> loadCache = this.stateLoadCache.get(stateRuleEnum);
        StateCollection stateCollection = loadCache.get(currentState);
        if (stateCollection == null) return Collections.emptyList();
        return Arrays.asList(stateCollection.getTransitionArray());
    }

    public boolean transition(StateRuleEnum stateRuleEnum, String currentState, String targetState) {
        Map<String, StateCollection> loadCache = this.stateLoadCache.get(stateRuleEnum);
        StateCollection stateCollection = loadCache.get(currentState);
        return Objects.nonNull(stateCollection) && stateCollection.transition(targetState);
    }

    public static class StateCollection extends HashSet<String> {
        private final StateCollection[] transitionArray;

        public StateCollection(StateCollection[] convertibleArray) {
            this.transitionArray = convertibleArray;
        }

        public StateCollection(Collection<? extends String> c) {
            super(c);
            this.transitionArray = null;
        }

        public boolean transition(String changeState) {
            for (StateCollection stateCollection : this.transitionArray) {
                if (stateCollection.contains(changeState)) {
                    return true;
                }
            }
            return false;
        }

        public StateCollection[] getTransitionArray() {
            return transitionArray;
        }
    }

    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(Dict.STATE_COLLECTION).append("\n");
        for (Entry<String, Set<String>> entry : this.stateSetCache.entrySet()) {
            stringBuilder.append("\t").append(entry.getKey()).append(" : ").append(entry.getValue()).append("\n");
        }
        for (Entry<StateRuleEnum, List<StateCollection>> entry : this.stateCollectionCache.entrySet()) {
            stringBuilder.append(entry.getKey().getConfig()).append("\n");
            for (StateCollection sc : entry.getValue()) {
                stringBuilder.append("\t").append(sc).append(" : ")
                        .append(Arrays.toString(sc.getTransitionArray())).append("\n");
            }
        }
        for (Entry<StateRuleEnum, Map<String, StateCollection>> entry : this.stateLoadCache.entrySet()) {
            stringBuilder.append(entry.getKey().getConfig()).append("\n");
            for (Entry<String, StateCollection> vEntry : entry.getValue().entrySet()) {
                stringBuilder.append("\t").append(vEntry.getKey()).append(" -> ")
                        .append(Arrays.toString(vEntry.getValue().getTransitionArray())).append("\n");
            }
        }
        return stringBuilder.toString();
    }
}
